package de.akabahn.ebd.stw.zdxfc.prs;

import static de.akabahn.ebd.comm.Util.sleep;
import static de.akabahn.ebd.comm.Util.waitForLatch;
import static de.akabahn.ebd.stw.zdxfc.prs.ZDXFCConstants.BGTASK_DELAY;
import static de.akabahn.ebd.stw.zdxfc.prs.ZDXFCConstants.BGTASK_INITIAL_DELAY;
import static de.akabahn.ebd.stw.zdxfc.prs.ZDXFCConstants.SPERRE_ZDXFC;

import java.util.HashMap;
import java.util.Map;
import java.util.Observable;
import java.util.Observer;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import javax.sound.sampled.LineUnavailableException;

import org.apache.log4j.Logger;

import de.akabahn.ebd.dbd.DBDAdapter;
import de.akabahn.ebd.dbd.DBDAdapterFactory;
import de.akabahn.ebd.stw.zdxfc.prs.block.Block1231;
import de.akabahn.ebd.stw.zdxfc.prs.elemente.Gleis;
import de.akabahn.ebd.stw.zdxfc.prs.elemente.Signal;
import de.akabahn.ebd.stw.zdxfc.prs.elemente.Weiche;
import de.akabahn.ebd.stw.zdxfc.prs.fst.FahrStrasse;

/**
 * Kontrollklasse zum Starten, Stoppen und Restarten des Stellwerks, Schnittstelle
 * zum DBD und Verwaltung aller Fahrweg-Elemente.
 * 
 * @author Peter Eimann
 * @since 1.0
 * @version 1.2
 * @see DBDAdapter
 * @see ZDXFCDispatcher
 * @see ZDXFCInitializer
 * @see ZDXFCConstants
 */
public class ZDXFCMain implements Observer {

	public final static String VERSION = "2.3.1_6";

	private final static Logger LOG = Logger.getLogger(ZDXFCMain.class);

	private DBDAdapter dbd;
	private Map<String, Weiche> weichen;
	private Map<String, Gleis> gleise;
	private Map<String, Signal> signale;
	private Map<String, FahrStrasse> fahrStrassen;
	private Block1231 block1231;

	private ZDXFCDispatcher dispatcher;
	private ZDXFCInitializer initializer;
	private ScheduledExecutorService backgroundService;
	private ScheduledFuture<?> backgroundFuture;

	private CountDownLatch exitLatch;

	private boolean restart;
	private boolean nowait;
	private boolean sperrUmgehung;

	private long fahrStrassenTasteSperrzeit = -1;

	/**
	 * Initialisierung der Klasse
	 * 
	 * @param nowait <code>true</code> - Sofortiges Beenden der Anwendung bei gesetzter Sperre
	 * @param sperrUmgehung <code>true</code> - Start der Anwendung auch bei gesetzter Sperre
	 * @throws LineUnavailableException 
	 */
	public ZDXFCMain(boolean nowait, boolean sperrUmgehung) throws LineUnavailableException {
		this.nowait = nowait;
		this.sperrUmgehung = sperrUmgehung;
		
		dbd = DBDAdapterFactory.getImplementation(this);
		weichen = new HashMap<String, Weiche>();
		gleise = new HashMap<String, Gleis>();
		signale = new HashMap<String, Signal>();
		fahrStrassen = new HashMap<String, FahrStrasse>();
		block1231 = new Block1231(this);
		
		dispatcher = new ZDXFCDispatcher(this);
		initializer = new ZDXFCInitializer(this, dispatcher);
		backgroundService = Executors.newSingleThreadScheduledExecutor();
		
		exitLatch = new CountDownLatch(1);
	}
	
	/**
	 * Hoch- und geordnetes Runterfahren des Stellwerks in der folgenden Reihenfolge:<br>
	 * <ol>
	 * <li>Starten des DBD-Adapters</li>
	 * <li>Pruefen, ob Stellwerk gestartet werden kann (keine Sperre gesetzt)</li>
	 * <li>Initialisierung des Stellwerks</li>
	 * <li>Starten des Background-Tasks (Scheduling mit regelmaessiger Ausfuehrung)</li>
	 * <li>Registrierung des Shutdown-Hooks</li>
	 * <li>Warten auf den Countdown-Latch zum Beenden des Programms (Schleife)</li>
	 * <li>Stoppen des DBD-Adapters</li>
	 * </ol>
	 * @see DBDAdapter#start()
	 * @see ZDXFCInitializer#initialize()
	 * @see ZDXFCBackgroundTask#run()
	 * @see DBDAdapter#stop()
	 * @see #pruefeSperre()
	 * @see #addShutdownHook()
	 */
	public void start() throws Exception {
		boolean latchFinished = false;
		
		LOG.info("Starting DBD Adapter...");
		dbd.start();
		
		pruefeSperre();
		
		LOG.info("Initialization started...");
		initializer.initialize();
		
		Runnable backgroundTask = new ZDXFCBackgroundTask(this);
		backgroundFuture = backgroundService.scheduleWithFixedDelay(backgroundTask, BGTASK_INITIAL_DELAY, BGTASK_DELAY, TimeUnit.MILLISECONDS);
		LOG.info("Initialization finished...");
		
		addShutdownHook();
		
		LOG.fatal("ZDXFCMain up and running");
		System.out.println("Chateauxprix up and running (Release " + VERSION + ")");
		
		do {
			try {
				latchFinished = waitForLatch(exitLatch, 5000);
			} catch (Exception e) {
				LOG.error("Exception caught in main loop", e);
				latchFinished = false;
			}
		} while (!latchFinished);
		
		LOG.fatal("Main loop terminated");
	}

	/**
	 * Restart und Neuinitialisierung des Stellwerks (ausgeloest durch entsprechende Hilfshandlung,
	 * siehe Konstante <code>RESET_STELLWERK</code>)
	 */
	public void restart() {
		LOG.warn("Restarting...");
		
		try {
			restart = true;
			
			LOG.info("Stopping DBD Adapter...");
			dbd.stop();
			
			weichen.clear();
			gleise.clear();
			signale.clear();
			fahrStrassen.clear();
			block1231 = new Block1231(this);
			
			LOG.info("Starting DBD Adapter...");
			dbd.start();
	
			LOG.info("Initialization started...");
			initializer.initialize();
			LOG.info("Initialization finished...");
		} catch (Exception e) {
			LOG.error("Exception during restart", e);
		} finally {
			restart = false;
			LOG.fatal("ZDXFCMain restarted");
		}
	}
	
	/**
	 * Zugriff auf den DBD-Adapter
	 * 
	 * @return DBD-Adapter
	 */
	public DBDAdapter getDbd() {
		return dbd;
	}

	/**
	 * Zugriff auf alle Weichen
	 * 
	 * @return Weichen (Key ist die ID der Weiche)
	 */
	public Map<String, Weiche> getWeichen() {
		return weichen;
	}
	
	/**
	 * Zugriff auf alle Gleise
	 * 
	 * @return Gleise (Key ist die ID des Gleises)
	 */
	public Map<String, Gleis> getGleise() {
		return gleise;
	}

	/**
	 * Zugriff auf alle Signale
	 * 
	 * @return Signale (Key ist die ID des Signals)
	 */
	public Map<String, Signal> getSignale() {
		return signale;
	}

	/**
	 * Zugriff auf alle Fahrstrassen
	 * 
	 * @return Fahrstrassen (Key ist die ID der Fahrstrasse)
	 */
	public Map<String, FahrStrasse> getFahrStrassen() {
		return fahrStrassen;
	}

	/**
	 * Zugriff auf den Block zwichen Chateauprix und Martinstein
	 * 
	 * @return Blockimplementierung Block ZDXFC<->ZDM
	 */
	public Block1231 getBlock1231() {
		return block1231;
	}

	/**
	 * Zugriff auf die Weiche mit der uebergebenen ID
	 * 
	 * @param id Die ID der Weiche
	 * @return Weiche mit der ID (oder <code>null</code> wenn die ID nicht gefunden wurde)
	 */
	public Weiche getWeiche(String id) {
		return getWeichen().get(id);
	}
	
	/**
	 * Hinzufuegen einer Weiche zur Weichen-Map
	 * 
	 * @param weiche Die Weiche, die hinzugefuegt werden soll (als Key wird die ID der Weiche verwendet)
	 */
	public void addWeiche(Weiche weiche) {
		getWeichen().put(weiche.getId(), weiche);
	}

	/**
	 * Zugriff auf das Gleis mit der uebergebenen ID
	 * 
	 * @param id Die ID des Gleises
	 * @return Gleis mit der ID (oder <code>null</code> wenn die ID nicht gefunden wurde)
	 */
	public Gleis getGleis(String id) {
		return getGleise().get(id);
	}

	/**
	 * Hinzufuegen eines Gleises zur Gleis-Map
	 * 
	 * @param gleis Das Gleis, das hinzugefuegt werden soll (als Key wird die ID des Gleises verwendet)
	 */
	public void addGleis(Gleis gleis) {
		getGleise().put(gleis.getId(), gleis);
	}
	
	/**
	 * Zugriff auf das Signal mit der uebergebenen ID
	 * 
	 * @param id Die ID des Signals
	 * @return Signal mit der ID (oder <code>null</code> wenn die ID nicht gefunden wurde)
	 */
	public Signal getSignal(String id) {
		return getSignale().get(id);
	}
	
	/**
	 * Hinzufuegen eines Signals zur Signal-Map
	 * 
	 * @param signal Das Signal, das hinzugefuegt werden soll (als Key wird die ID des Signals verwendet)
	 */
	public void addSignal(Signal signal) {
		getSignale().put(signal.getId(), signal);
	}
	
	/**
	 * Zugriff auf die Fahrstrasse mit der uebergebenen ID
	 * 
	 * @param id Die ID der Fahrstrasse
	 * @return Fahrstrasse mit der ID (oder <code>null</code> wenn die ID nicht gefunden wurde)
	 */
	public FahrStrasse getFahrStrasse(String id) {
		return getFahrStrassen().get(id);
	}
	
	/**
	 * Hinzufuegen einer Fahrstrasse zur Fahrstrassen-Map
	 * 
	 * @param fahrStrasse Die Fahrstrasse, die hinzugefuegt werden soll (als Key wird die ID der Fahrstrasse verwendet)
	 */
	public void addFahrStrasse(FahrStrasse fahrStrasse) {
		getFahrStrassen().put(fahrStrasse.getId(), fahrStrasse);
	}
	
	/**
	 * @return the fahrStrassenTasteSperrzeit
	 */
	public long getFahrStrassenTasteSperrzeit() {
		return fahrStrassenTasteSperrzeit;
	}

	/**
	 * @param fahrStrassenTasteSperrzeit the fahrStrassenTasteSperrzeit to set
	 */
	public void setFahrStrassenTasteSperrzeit(long fahrStrassenTasteSperrzeit) {
		this.fahrStrassenTasteSperrzeit = fahrStrassenTasteSperrzeit;
	}

	/**
	 * Zugriff auf das Restart-Flag
	 * 
	 * @return Das Restart-Flag
	 */
	public boolean isRestart() {
		return restart;
	}

	/**
	 * Verbindungsinformation des DBD-Adapters, wird beim Wechsel des Verbindungsstatus aufgerufen
	 * 
	 * @param observable Das Adapter-Objekt
	 * @param object Zustand der Verbindung (true, false)
	 */
	@Override
	public void update(Observable observable, Object object) {
		LOG.warn("Connected to DBD: " + object.toString());
	}

	/**
	 * Abfrage der DBD Variablen <code>CHATEAUESTW</code> zur Pruefung ob das Stellwerk zur Steuerung
	 * freigegeben ist (Wert = 0) oder nicht (Wert != 0).<br>
	 * Ist der Wert ungleich 0 (Steuerung durch ESTW), wird geprueft, ob die Sperrumgehung aktiv ist. Falls ja, wird
	 * das Stellwerk trotz gesetzer Sperre gestartet (mit entsprechender Warnung im Logfile).<br>
	 * Ist die Sperrumgehung nicht gesetzt, wird geprueft, ob das <code>nowait</code>-Flag gesetzt ist. Falls ja wird 
	 * eine <code>IllegalStateException</code> geworfen, was die Beendigung der Anwendung zur Folge hat.<br>
	 * Ist das Flag nicht gesetzt, wird nach 5 Sekunden erneut geprueft, ob die Sperre inzwischen freigeben wurde 
	 * oder nicht.
	 * 
	 * @throws IllegalStateException Wird geworfen, wenn die Sperre gesetzt und die Anwendung mit dem nowait-Flag
	 *                               gestartet wurde
	 */
	private void pruefeSperre() throws IllegalStateException {
		int sperre = 0;
		boolean printLogMessage = true;

		do {
			LOG.info("Pruefe Sperre...");
			sperre = dbd.query(SPERRE_ZDXFC).getValue();
			if (sperre != 0) {
				if (sperrUmgehung) {
					LOG.warn("Sperrumgehung aktiv, " + SPERRE_ZDXFC + " = " + sperre);
				} else {
					if (nowait) {
						dbd.stop();
						throw new IllegalStateException(SPERRE_ZDXFC + " = " + sperre);
					} else {
						if (printLogMessage) {
							printLogMessage = false;
							LOG.warn("Sperre aktiv, warte auf Freigabe...");
							System.out.println("Sperre aktiv, warte auf Freigabe...");
						}
						sleep(5000);
					}
				}
			} else {
				LOG.info("Chateauprix freigegeben");
			}
		} while (sperre != 0);
	}

	/**
	 * Registrierung eines Shutdown-Hooks, das auf die Beendigung der Anwendung (z.B. durch &lt;STRG&gt;+C)
	 * reagiert und die Anwendung geordnet beendet (Stoppen des DBD-Adapters zur Trennung der Verbindung zum DBD).
	 */
	private void addShutdownHook() {
		Runtime.getRuntime().addShutdownHook(new Thread() {
			public void run() {
				LOG.info("Shutting down, <STRG>+C received");
				backgroundFuture.cancel(true);
				backgroundService.shutdown();
				exitLatch.countDown();

				LOG.info("Stopping DBD Adapter...");
				try {
					dbd.stop();
					Thread.sleep(1000);
				} catch (Exception e) {
				}

				LOG.fatal("ZDXFCMain terminated");
			}
		});
	}
}
